package tuioZones;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;

import oscP5.OscMessage;
import oscP5.OscP5;
import processing.core.PApplet;
import processing.core.PConstants;

public class GamingArea implements PConstants{

	private final int CARDS_TO_BE_DISPLAYED_PER_CATEGORY = 12;
	public static final int CARD_WIDTH = 100; // in pixel 100px = 10 cm
	public static final int CARD_HEIGHT = 100; // in pixel 100px = 10 cm
	public static final int CARD_FUSION = 15; // if cards are less then 15 px > fusion them

	private PApplet myParent;
	private float height;
	private float width;
	private ArrayList<Category> categories;
	private GamingZone gamingZone;
	
	private ArrayList<TouchZone> touchZones;
	private ArrayList<TouchPoint> touchPoints;
	
	private DropArea[] dropAreas;
	
	private ArrayList<Card> cardsToDisplay;
	
	public GamingArea(PApplet pApplet) {
		this(pApplet,3333);
	}

	public GamingArea(PApplet pApplet, int port) {
		this.myParent = pApplet;
		this.height = myParent.height;
		this.width = myParent.width;
		// register for tuio
		new OscP5(this, port);
		this.touchZones = new ArrayList<TouchZone>();
		this.touchPoints = new ArrayList<TouchPoint>();
	
		dropAreas = new DropArea[4];
		
		float height_div = 6;
		
		dropAreas[0] = new DropArea(0, height / 2 - width / 8, height / height_div, width / 4, 240,29,36, 90); // red
		dropAreas[1] = new DropArea(width / 2 - width / 8, 0, width / 4, height / height_div, 57,175,83, 180); // green
		dropAreas[2] = new DropArea(width - height / height_div - 1, height / 2 - width / 8, height / height_div, width / 4,237,203,67, 270); // yellow
		dropAreas[3] = new DropArea(width / 2 - width / 8, height - height / height_div - 1, width / 4, height / height_div,18,120,209, 0); // blue
		
		initializeGamingZone();
		findCategories();
		getInitialCardsToDisplay();
		
		myParent.registerPre(this);
	}
	

	private void getInitialCardsToDisplay() {
		cardsToDisplay = new ArrayList<Card>();
		
		for(Category c : categories) {
			cardsToDisplay.addAll(c.getInitialSetOfCards(CARDS_TO_BE_DISPLAYED_PER_CATEGORY));
		}

		touchZones = new ArrayList<TouchZone>();
		for(Card c : cardsToDisplay) {
			PointXY px = new PointXY((float)(gamingZone.getX() + gamingZone.getWidth() * Math.random()),(float)(gamingZone.getY() + gamingZone.getHeight() * Math.random()));
			// point should be in gamingZone
			// FIXME
			while(!(gamingZone.containsPoint(px) && 
					gamingZone.containsPoint(new PointXY(px.getX() + CARD_WIDTH, px.getY() + CARD_HEIGHT)) &&
					gamingZone.containsPoint(new PointXY(px.getX(),px.getY() + CARD_HEIGHT)) &&
					gamingZone.containsPoint(new PointXY(px.getX() + CARD_WIDTH, px.getY()))))
				px = new PointXY((float)(gamingZone.getX() + gamingZone.getWidth() * Math.random()),(float)(gamingZone.getY() + gamingZone.getHeight() * Math.random()));
			// create the touchzone with the specific card
			TouchZone t = new TouchZone(myParent, px.getX(), px.getY(), CARD_WIDTH, CARD_HEIGHT);
			t.setCard(c);
			touchZones.add(t);
		}
	}

    /**
     * Adds one new pair of Cards from a specified category to the game
     */
    private void addNewCard(Category category) {
        cardsToDisplay = category.getInitialSetOfCards(2);

        for(Card c : cardsToDisplay) {
			PointXY px = new PointXY((float)(gamingZone.getX() + gamingZone.getWidth() * Math.random()),(float)(gamingZone.getY() + gamingZone.getHeight() * Math.random()));
			// point should be in gamingZone
			// FIXME
			while(!(gamingZone.containsPoint(px) &&
					gamingZone.containsPoint(new PointXY(px.getX() + CARD_WIDTH, px.getY() + CARD_HEIGHT)) &&
					gamingZone.containsPoint(new PointXY(px.getX(),px.getY() + CARD_HEIGHT)) &&
					gamingZone.containsPoint(new PointXY(px.getX() + CARD_WIDTH, px.getY()))))
				px = new PointXY((float)(gamingZone.getX() + gamingZone.getWidth() * Math.random()),(float)(gamingZone.getY() + gamingZone.getHeight() * Math.random()));
			// create the touchzone with the specific card
			TouchZone t = new TouchZone(myParent, px.getX(), px.getY(), CARD_WIDTH, CARD_HEIGHT);
			t.setCard(c);
			touchZones.add(t);
		}
    }

	private void initializeGamingZone() {
		gamingZone = new GamingZone(height/5, height / 5, width - 2 * height / 5, 3 * height / 5, 12,34,56);
	}

	private void findCategories() {
		categories = new ArrayList<Category>();
		
		File dir = new File(Category.library);
		
		if(dir.exists()) {
			File[] list = dir.listFiles();
			for(File category : list) {
				if(category.isDirectory() && !category.isHidden())
					categories.add(new Category(myParent, category));
			}
		}
	}

	public DropArea[] getDropAreas() {
		return this.dropAreas;
	}
	
	public GamingZone getGamingZone() {
		return gamingZone;
	}
	
	/**
	 * called by Processing at the beginning of each draw
	 */
	public void pre() {
		// could be removed
		for(DropArea da : dropAreas) {
			if(da.getTouchPoints().size() > 0)
				Logger.println("DropArea contains " + da.getTouchPoints().size() + " TouchPoints");
		}
		// could be removed
		Collections.sort(touchZones);
		for(TouchZone tz : touchZones) {
			if(tz.getTouchPoints().size() != 2) {
				tz.setRotationPoint(null);
			}
		}

		// Fusion STUFF
        ArrayList<TouchZone> zonesToDelete = new ArrayList<TouchZone>();
		 for(TouchZone tz : touchZones) {
	          String cardName = tz.getCard().getIdentifier();
	          String aOrB = cardName.substring(cardName.length()-1);
	          cardName = cardName.substring(0,cardName.length()-2);
	          for(TouchZone tz2 : touchZones) {
	              String cardName2 = tz2.getCard().getIdentifier();
	              String aOrB2 = cardName2.substring(cardName2.length()-1);
	              cardName2 = cardName2.substring(0,cardName2.length()-2);
	              /**
	               * manche karten k�nnen auch auf beiden seiten zusammengef�gt werden.
	               */

	              if (cardName.startsWith("~")) {
	                  if (cardName.equals(cardName2)&&(!aOrB.equals(aOrB2))) {
	                      if ((tz.getCoordB().distanceTo(tz2.getCoordA())<CARD_FUSION)&&(tz.getCoordC().distanceTo(tz2.getCoordD())<CARD_FUSION)) {
	                          tz2.setAngle(tz.getAngle());
	                          tz2.setX(tz.getCoordB().getX());
	                          tz2.setY(tz.getCoordB().getY());
                              Puzzle p = new Puzzle(cardName);
                              p.setCA(tz.getCard());
                              p.setCB(tz2.getCard());
                              p.setCategory(tz.getCard().getCategory());
                              tz.setPuzzle(p);
                              tz.setAngle(tz.getAngle());
                              //touchZones.remove(tz2);                              
                              zonesToDelete.add(tz2);
	                      } else if ((tz2.getCoordB().distanceTo(tz.getCoordA())<CARD_FUSION)&&(tz2.getCoordC().distanceTo(tz.getCoordD())<CARD_FUSION)) {
	                          tz.setAngle(tz2.getAngle());
	                          tz.setX(tz2.getCoordB().getX());
	                          tz.setY(tz2.getCoordB().getY());
                              Puzzle p = new Puzzle(cardName);
                              p.setCA(tz2.getCard());
                              p.setCB(tz.getCard());
                              p.setCategory(tz2.getCard().getCategory());
                              tz2.setPuzzle(p);
                              tz2.setAngle(tz2.getAngle());
                              //touchZones.remove(tz2);
                              zonesToDelete.add(tz);
	                      }
	                  }
	              } else {
                      if (cardName.equals(cardName2)&&(!aOrB.equals(aOrB2))) {
	                      //linke karte A, rechte B
                          if (aOrB.equals("A")) {
	                          if ((tz.getCoordB().distanceTo(tz2.getCoordA())<CARD_FUSION)&&(tz.getCoordC().distanceTo(tz2.getCoordD())<CARD_FUSION)) {
                                  tz2.setAngle(tz.getAngle());
	                              tz2.setX(tz.getCoordB().getX());
	                              tz2.setY(tz.getCoordB().getY());
                                  Puzzle p = new Puzzle(cardName);
                                  p.setCA(tz.getCard());
                                  p.setCB(tz2.getCard());
                                  p.setCategory(tz.getCard().getCategory());
                                  tz.setPuzzle(p);
                                  tz.setAngle(tz.getAngle());
                                  //touchZones.remove(tz2);
                                  zonesToDelete.add(tz2);
	                          }

	                      } else if (aOrB.equals("B")) {

	                      }
	                  }
	              }                          
	          }
	      }
         // if a puzzle is found, remove the second zone
         for (TouchZone ta : zonesToDelete) {
             touchZones.remove(ta);
         }
		
		// ROTATION STUFF
		for(TouchZone tz : touchZones) {
			if(tz.getTouchPoints().size() == 2) {
				try {
					// die 2 touchpoints von der touchzone
					// primary
					TouchPoint tp1 = tz.getTouchPoints().get(0);
					// secundary
					TouchPoint tp2 = tz.getTouchPoints().get(1);
					
					// aktuellen Koordinaten
					PointXY akt1 = tp1.getLastPoint();
					PointXY akt2 = tp2.getLastPoint();
					
					// previous Koordinaten
					PointXY prev1 = tp1.getHistory().get(tp1.getHistory().size() - 2);
					PointXY prev2 = tp2.getHistory().get(tp2.getHistory().size() - 2);
					
					// steigungsvektoren
					double vPrev = ((prev1.getX() - prev2.getX())) / ((prev1.getY() - prev2.getY()));
					double vAkt = ((akt1.getX() - akt2.getX())) / ((akt1.getY() - akt2.getY()));
					
					if(!Double.isNaN(vPrev) && !Double.isNaN(vAkt) && !Double.isInfinite(vPrev) && !Double.isInfinite(vAkt)) {

						double kPrev = Math.atan(vPrev); // letzte steigung
						double kAkt = Math.atan(vAkt); // aktuelle steigung
						
						float kDiff = (float)(kPrev - kAkt);
						
						//System.out.println((float)((int)(Math.toDegrees(kPrev)*1000))/10 + " " + (float)((int)(Math.toDegrees(kAkt)*1000))/10 + " " + Math.toDegrees(kDiff));
						/**  */
						if(Math.toDegrees(kDiff) < -90) {
							kDiff = (float)(Math.toRadians(-180) + kAkt - kPrev);
							//System.out.println(" " + Math.toDegrees(kPrev) + " " + Math.toDegrees(kAkt) + " " + Math.toDegrees(kDiff));
						}
						if(Math.toDegrees(kDiff) > 90) {
							kDiff = (float)(kAkt - kPrev - Math.toRadians(180));
							//System.out.println(Math.toDegrees(kPrev) + " " + Math.toDegrees(kAkt) + " " + Math.toDegrees(kDiff));
						}
						if(kDiff != 0) {
							// rotationpoints
							PointXY rot1 = getMiddlePoint(prev1,prev2);
							PointXY rot2 = getMiddlePoint(akt1,akt2);
							
							// vektor zu A(prev)
							float toXA = (float)(tz.getX() - rot1.getX());
							float toYA = (float)(tz.getY() - rot1.getY());
							
							// vektor zu A(prev) um kDiff transformiert
							float xdiff = (float)(toXA * Math.cos(kDiff) - toYA * Math.sin(kDiff)); 
							float ydiff = (float)(toYA * Math.cos(kDiff) + toXA * Math.sin(kDiff));
							
							// vektor von Rotationspunkt(akt) zu A(akt)
							tz.setX((float)(rot2.getX() + xdiff));
							tz.setY((float)(rot2.getY() + ydiff));

							// rotate XY grad
							tz.rotate((float)Math.toDegrees(kDiff));
							
							// just for debugging help
							tz.setRotationPoint(rot2);
						}
					}
				} catch( Exception e) {
					//e.printStackTrace();
				}
			}
		}

        zonesToDelete = new ArrayList<TouchZone>();
        // puzzle in DropArea
        for (TouchZone tz : touchZones) {
            //touchzone contains a puzzle
            if (tz.getPuzzle()!=null) {
                //check if droparea contains touchzone                
                for (DropArea dz : dropAreas) {
                    //for every touchpoint in touchzone
                    for (TouchPoint tp : tz.getTouchPoints()) {
                        // puzzle in drop zone
                        // only if dropZone ist not full
                        if (dz.contains(tp) && (dz.getFinishedPuzzles().size()<dz.getMaxPuzzles())) {
                            dz.addPuzzle(tz.getPuzzle());
                            zonesToDelete.add(tz);                                                        
                        }
                    }//for every touchpoint
                }//for (DropArea dz)
            }//if (tz.getPuzzle
        }// for (TouchArea
         // if a puzzle in dropArea, remove zone and create a new pair of zones with new cards
         for (TouchZone ta : zonesToDelete) {
             touchZones.remove(ta);
             addNewCard(ta.getPuzzle().getCategory());
         }
	}
	
	/**
	 * returns the middle point between two points
	 * @param p1
	 * @param p2
	 * @return
	 */
	private PointXY getMiddlePoint(PointXY p1, PointXY p2) {
		return new PointXY(Math.max(p1.getX(), p2.getX()) - (Math.max(p1.getX(), p2.getX()) - Math.min(p1.getX(), p2.getX())) / 2, Math.max(p1.getY(), p2.getY()) - (Math.max(p1.getY(), p2.getY()) - Math.min(p1.getY(), p2.getY())) / 2);
	}

	public ArrayList<TouchPoint> getTouchPoints() {
		return touchPoints;
	}
	

	/**
	 * @return the touchZones
	 */
	public ArrayList<TouchZone> getTouchZones() {
		return touchZones;
	}
	
	/**
	 * Handle TUIO events
	 * @param theOscMessage
	 */
	void oscEvent(OscMessage theOscMessage) {

		if (theOscMessage.get(0).stringValue().equals("alive")) {
			
			int sid;
			boolean found;
			// cycle through points
			
			for(int i=0; i < touchPoints.size(); i++) {
				TouchPoint tp = touchPoints.get(i);
				sid = tp.getSID();
				found = false;
				// see if point is alive
				if (!found) {
					// cycle through alive sid's
					for (int j = 1; j < theOscMessage.arguments().length; j++) {
						if (theOscMessage.get(j).intValue() == sid) {
							found = true;
							break;
						}
					}
				}
				// if point is not alive, kill it
				if (!found) {
					// if point is in a zone -> remove the point inside the zone
					for(TouchZone tz : touchZones) {
						if(tz.getTouchPoints().contains(tp)) {
							tz.removeTouchPoint(tp);
							break;
						}
					}
					
					for(DropArea da : dropAreas) {
						if(da.getTouchPoints().contains(tp)) {
							da.removeTouchPoint(tp);
							break;
						}
					}
					touchPoints.remove(i);
				}
			}
		}
		// //////////////////////////////////////modify or add////////////////
		if (theOscMessage.typetag().startsWith("sifffff") == true) {
			int sid = theOscMessage.get(1).intValue();
			float xIn = (myParent.width * theOscMessage.get(2).floatValue());
			float yIn = (myParent.height * theOscMessage.get(3).floatValue());
			boolean modified = false;
			TouchPoint tempPoint = null;
			for (TouchPoint tp : touchPoints) {
				if (tp.getSID() == sid) {
					tempPoint = tp;
				}
			}

			// update known point using sid
			if (tempPoint != null) {
				modified = true;
				// move the zone!!!
				if(tempPoint.getZone() != null) {
					TouchZone tz = tempPoint.getZone();
					if(tz.getTouchPoints().size() == 1) {
						tz.setX(tz.getX() + (xIn - tz.getPrimary().getX()));
						tz.setY(tz.getY() + (yIn - tz.getPrimary().getY()));
					}
				}
				
				tempPoint.addPoint(sid, xIn, yIn);
			}
			// create new touch point if not found
			if(!modified) {
				
				TouchZone tz = null;
				for(int i=touchZones.size() - 1; i >= 0; i--) {
					TouchZone tzone = touchZones.get(i);
					if(tzone.contains(new PointXY(xIn,yIn))) {
						tz = tzone;
						break;
					}
				}
				TouchPoint tp = new TouchPoint(myParent, sid, xIn, yIn,tz);
				touchPoints.add(tp);
				if(tz != null) {
					tz.addTouchPoint(tp);
				}
			}
		}
		
		analyzePoints();
		/*
		 * store the last actions in the zones!!!
		 * so that the last zone, which was used will be displayed on top
		 * and if a touchzone is active -> should be on top!
		 */
	}

	private void analyzePoints() {		
		for(TouchPoint tp : touchPoints) {
			for(DropArea da : dropAreas) {
				if(da.contains(tp)) {
					da.addTouchPoint(tp);
				} else {
					da.removeTouchPoint(tp);
				}
			}
		}
		
		Collections.sort(touchZones);
	}

}
